package org.cuit.seon.service;


import com.google.gson.Gson;
import lombok.Data;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;
import org.cuit.seon.core.context.ApiConfiguration;
import org.cuit.seon.core.context.HeaderKeys;
import org.cuit.seon.core.context.TokenCache;
import org.cuit.seon.core.data.R;
import org.cuit.seon.core.response.LoginVO;

import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Data
public class RequestExecutor {
    private static final Gson gson = new Gson();

    private final ApiConfiguration config;

    private final OkHttpClient client;

    private final TokenCache tokenCache = new TokenCache();

    private final Map<String, Integer> tokenMap = new ConcurrentHashMap<>();

    private final Object lock = new Object();

    public RequestExecutor(ApiConfiguration config) {
        this.client = new OkHttpClient();
        this.config = config;
    }

    public Map<String, Integer> getTokenMap() {
        return tokenMap;
    }

    /**
     * get方法执行
     */
    public <T> R<T> get(String URI, Map<String, String> queryParameter, Type type) {
        String URL = generateURL(URI, queryParameter);
        Request request = new Request.Builder()
                .get()
                .url(URL)
                .build();
        return executeNeedAuth(request, type);
    }

    /**
     * post方法执行
     * 参数使用json传递
     */
    public <T> R<T> post(String URI, Map<String, String> queryParameter, Object body, Type type) {
        String URL = generateURL(URI, queryParameter);
        MediaType JSON = MediaType.parse("application/json;charset=utf-8");
        Request request = new Request.Builder()
                .post(RequestBody.create(JSON, gson.toJson(body)))
                .url(URL)
                .build();
        return executeNeedAuth(request, type);
    }

    /**
     * URL生成
     */
    private String generateURL(String URI, Map<String, String> query) {
        // 构建搜索参数
        StringBuilder URL = new StringBuilder(config.getServerURL() + URI);
        if (Objects.nonNull(query)) {
            Set<Map.Entry<String, String>> entries = query.entrySet();
            if (entries.size() > 0) {
                URL.append("?");
                for (Map.Entry<String, String> entry : entries) {
                    if (entry.getValue() != null) {
                        URL.append("&").append(entry.getKey()).append("=").append(entry.getValue());
                    }
                }
            }
        }
        return URL.toString();
    }

    /**
     * 执行请求
     */
    private <T> R<T> execute(Request request, Type type) {
        Call call = client.newCall(request);
        String token = request.header("token");
        if (token != null) {
            tokenMap.merge(token, 1, Integer::sum);
        }
        try (Response response = call.execute()) {
            if (response.body() != null) {
                Type typeOfT = new ParameterizedType() {

                    @Override
                    public Type[] getActualTypeArguments() {
                        Type[] t = new Type[1];
                        t[0] = type;
                        return t;
                    }

                    @Override
                    public Type getRawType() {
                        return R.class;
                    }

                    @Override
                    public Type getOwnerType() {
                        return null;
                    }
                };

                return gson.fromJson(response.body().string(), typeOfT);
            }
            return null;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 为请求添加认证信息并且执行
     */
    private <T> R<T> executeNeedAuth(Request handle, Type type) {
        String oldToken = getToken();
        handle = handle.newBuilder()
                .header(HeaderKeys.USER_TOKEN, oldToken)
                .build();
        R<T> result = execute(handle, type);
        if (Objects.nonNull(result) && result.getCode() == 401) {
            // 401 未登录说明token过期，移除老缓存, 并且只能移除之前的token
            tokenCache.remove(oldToken);
            // 只有重新获取token需要被锁
            synchronized (lock) {
                getToken();
            }
            handle = handle.newBuilder()
                    .header(HeaderKeys.USER_TOKEN, getToken())
                    .build();
            result = execute(handle, type);
        }
        return result;
    }

    /**
     * 拿取当前token
     */
    private String getToken() {
        // 双重检查锁，如果cache中的确没有有效token，则直接刷新token
        String cacheToken = cacheToken();
        if (StringUtils.isNotBlank(cacheToken)) {
            return cacheToken;
        }
        synchronized (lock) {
            cacheToken = cacheToken();
            if (StringUtils.isBlank(cacheToken)) {
                return refreshToken();
            }
            return cacheToken;
        }
    }

    /**
     * 刷新token
     */
    @SuppressWarnings("all")
    private String refreshToken() {
        Map<String, String> loginForm = new HashMap<>();
        loginForm.put("account", config.getAuthAccount());
        loginForm.put("password", config.getAuthPassword());
        MediaType JSON = MediaType.parse("application/json;charset=utf-8");
        Request request = new Request.Builder()
                .post(RequestBody.create(JSON, gson.toJson(loginForm)))
                .url(config.getServerURL() + "/user/pc/login")
                .build();
        R<LoginVO> result = this.execute(request, LoginVO.class);
        String token = result.getData().getToken();
        if (StringUtils.isBlank(token)) {
            throw new RuntimeException("登录失败,token无法获取,请检查账号密码后重试");
        }
        tokenCache.set(token);
        return token;
    }

    /**
     * 拿取缓存中token
     */
    private String cacheToken() {
        try {
            TimeUnit.MILLISECONDS.sleep(1L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return tokenCache.get();
    }
}
